﻿using System;
using Heaven.Mathematics;

namespace Heaven.Engine
{
    /// <summary>
    /// Represent low-level perspective camera
    /// </summary>
    public class CameraPerspective : Camera
    {
        #region Cache

        // Projection matrix
        bool projectionMatrixCached = false;
        Matrix4 projectionMatrix;
        // View matrix
        bool viewMatrixCached = false;
        Matrix4 viewMatrix;

        #endregion

        #region Fields

        /// <summary>
        /// Field of view
        /// </summary>
        protected double fov = (float)(Math.PI / 3.0f);
        /// <summary>
        /// A ratio of width to height of the frame
        /// </summary>
        protected double aspect = 4.0f / 3.0f;
        /// <summary>
        /// The distance to near clipping plane
        /// </summary> 
        protected double near = 1.0f;
        /// <summary>
        /// The distance to far clipping plane
        /// </summary> 
        protected double far = 50000.0f;
        /// <summary>
        /// Position of the observer
        /// </summary>
        protected Vector3 position = Vector3.Zero;
        /// <summary>
        /// Camera target
        /// </summary>
        protected Vector3 target = new Vector3(10,0,0);
        /// <summary>
        /// Defines up of the camera
        /// </summary>
        protected Vector3 up = new Vector3(0, 10, 0);

        #endregion

        #region Properties

        /// <summary>
        /// Field of view
        /// </summary>
        public virtual double Fov
        {
            get
            {
                return fov;
            }
            set
            {
                fov = value;
                Invalidate();
            }
        }

        /// <summary>
        /// A ratio of width to height of the frame
        /// </summary>
        public virtual double Aspect
        {
            get
            {
                return aspect;
            }
            set
            {
                aspect = value;
                Invalidate();
            }
        }

        /// <summary>
        /// The distance to near clipping plane
        /// </summary>
        public virtual double Near
        {
            get
            {
                return near;
            }
            set
            {
                near = value;
                Invalidate();
            }
        }

        /// <summary>
        /// The distance to far clipping plane
        /// </summary>
        public virtual double Far
        {
            get
            {
                return far;
            }
            set
            {
                far = value;
                Invalidate();
            }
        }

        /// <summary>
        /// Gets or sets position of the observer
        /// </summary>
        public virtual Vector3 Position
        {
            get
            {
                return position;
            }
            set
            {
                position = value;
                Invalidate();
            }
        }

        /// <summary>
        /// Gets or sets the camera target
        /// </summary>
        public virtual Vector3 Target
        {
            get { return target; }
            set
            {
                target = value;
                Invalidate();
            }
        }

        /// <summary>
        /// Gets or sets up vector which defines up orientation of the camera
        /// </summary>
        public virtual Vector3 Up
        {
            get { return up; }
            set
            {
                up = value;
                Invalidate();
            }
        }


        /// <summary>
        /// The final matrix of view
        /// </summary>
        public override Matrix4 View
        {
            get
            {
                if(!viewMatrixCached) 
                {
                    viewMatrix = Matrix4.LookAt(position, target, up);
                    viewMatrixCached = true;
                }
                return viewMatrix;
            }
        }

        /// <summary>
        /// The final projection matrix
        /// </summary>
        public override Matrix4 Projection
        {
            get
            {
                if (!projectionMatrixCached) 
                {
                    projectionMatrix = Matrix4.Frustum(fov, aspect, near, far);
                    projectionMatrixCached = true;
                }
                return projectionMatrix;
            }
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Default constructor
        /// </summary>
        public CameraPerspective()
        {
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="fov">Field of view</param>
        /// <param name="aspect"> A ratio of width to height of the frame</param>
        /// <param name="near">The distance to near clipping plane</param>
        /// <param name="far">The distance to far clipping plane</param>
        /// <param name="position">Position of the observer</param>
        /// <param name="target">Camera target</param>
        /// <param name="up">Defines up of the camera</param>
        public CameraPerspective(double fov, double aspect, double near, double far, Vector3 position, Vector3 target, Vector3 up)
        {
            this.fov = fov;
            this.aspect = aspect;
            this.near = near;
            this.far = far;
            this.position = position;
            this.target = target;
            this.up = up;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Invalidates object
        /// </summary>
        public override void Invalidate()
        {
            // Reset cache
            projectionMatrixCached = false;
            viewMatrixCached = false;
            // Invoke the base method
            base.Invalidate();
        }

        /// <summary>
        /// Makes a ray from camera
        /// </summary>
        /// <param name="camera">A camera</param>
        /// <param name="x">Normalized coordinate X of view [0,1]</param>
        /// <param name="y">Normalized coordinate Y of view [0,1]</param>
        /// <returns>Ray</returns>
        public static Ray MakeRay(CameraPerspective camera, double x, double y)
        {
            Vector3 rayDirection = Vector3.Zero;
            Vector3 rayOrigin = Vector3.Zero;

            Matrix4 view = Matrix4.Inverse(camera.View);
            Matrix4 projection = Matrix4.Frustum(camera.Fov, camera.Aspect, camera.Near, camera.Far);

            Vector3 v = Vector3.Zero;
            v.X = (x * 2.0f - 1.0f) / projection.M11;
            v.Y = -(y * 2.0f - 1.0f) / projection.M22;
            v.Z = -camera.Near;

            // Transforms our vector with the inversed view matrix
            rayDirection.X = v.X * view.M11 + v.Y * view.M21 + v.Z * view.M31;
            rayDirection.Y = v.X * view.M12 + v.Y * view.M22 + v.Z * view.M32;
            rayDirection.Z = v.X * view.M13 + v.Y * view.M23 + v.Z * view.M33;

            // Gets the eye position
            rayOrigin.X = view.M41;
            rayOrigin.Y = view.M42;
            rayOrigin.Z = view.M43;

            return new Ray(rayOrigin, rayDirection);
        }

        #endregion
    }
}
